﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Threading;
using be.metanous.mvvm.core;

namespace be.metanous.mvvm.collections
{
    
     /// <summary>
    /// Represents an <see cref="ObservableCollection{T}"/> that has ability to suspend
    /// change notification events. Notifications are send on the listner's thread!
    /// </summary>
    /// <typeparam name="T">The type of the items in the collection.</typeparam>
    [Serializable]
    public class DispatcherObservableCollection<T> : ObservableCollection<T>, ISuspendNotifications
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DispatcherObservableCollection&lt;T&gt;"/> class.
        /// </summary>
        public DispatcherObservableCollection()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DispatcherObservableCollection&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="collection">The collection from which the elements are copied.</param>
        /// <exception cref="T:System.ArgumentNullException">
        /// The <paramref name="collection"/> parameter cannot be null.
        /// </exception>
        public DispatcherObservableCollection(IEnumerable<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            CopyFrom(collection);
        }

        private void CopyFrom(IEnumerable<T> collection)
        {
            if (collection == null || Items == null)
                return;

            using (IEnumerator<T> enumerator = collection.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    Items.Add(enumerator.Current);
                }
            }
        }

        /// <summary>
		/// Occurs when collection is changing. (Cancelable)
		/// </summary>
        [field:NonSerialized]
		public event EventHandler<CollectionChangingEventArgs> CollectionChanging;

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public new event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                base.PropertyChanged += value;
            }
            remove
            {
                base.PropertyChanged -= value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether change to the collection is made when
        /// its notifications are suspended.
        /// </summary>
        /// <value><c>true</c> if this instance is has been changed while notifications are
        /// suspended; otherwise, <c>false</c>.</value>
        protected bool IsDirty { get; set; }

        /// <summary>
        /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
        /// </summary>
        /// <param name="items">The items that will be added.</param>
        /// <exception cref="ArgumentNullException"><paramref name="items"/> is null</exception>
        public virtual void AddRange(IEnumerable<T> items)
        {
            if (items == null)
            {
                throw new ArgumentNullException("items");
            }

            foreach (T item in items)
            {
                Add(item);
            }
        }


        /// <summary>
        /// Raises <see cref="INotifyCollectionChanged.CollectionChanged"/> with 
        /// <see cref="NotifyCollectionChangedAction.Reset"/> changed action.
        /// </summary>
        public void Reset()
        {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        // Override the event so this class can access it
        public override event NotifyCollectionChangedEventHandler CollectionChanged;

        /// <summary>
        /// Raises the <see cref="INotifyCollectionChanged.CollectionChanged"/> event when
        /// notifications are not suspended.
        /// </summary>
        /// <param name="e">The event data to report in the event.</param>
        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (!NotificationsSuspended)
            {
                // base.OnCollectionChanged(e);
                // Be nice - use BlockReentrancy like MSDN said
                using (BlockReentrancy())
                {
                    NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
                    if (eventHandler == null)
                        return;

                    Delegate[] delegates = eventHandler.GetInvocationList();
                    // Walk thru invocation list
                    foreach (NotifyCollectionChangedEventHandler handler in delegates)
                    {
                        var dispatcherObject = handler.Target as DispatcherObject;
                        // If the subscriber is a DispatcherObject and different thread
                        if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
                        {
                            // Invoke handler in the target dispatcher's thread
                            dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
                        }
                        else // Execute handler as is
                            handler(this, e);
                    }
                }

            }
            else
            {
                IsDirty = true;
            }
        }

        /// <summary>
        /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event when
        /// notifications are not suspended.
        /// </summary>
        /// <param name="e">Arguments of the event being raised.</param>
        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (!NotificationsSuspended)
            {
                base.OnPropertyChanged(e);
            }
        }

         /// <summary>
         /// Raises the <see cref="CollectionChanging"></see>
         ///   event.
         /// </summary>
         /// <param name="e">The <see cref="CollectionChangingEventArgs"/> instance containing the event data.</param>
         protected virtual void OnCollectionChanging(CollectionChangingEventArgs e)
		{
			if (CollectionChanging != null)
				CollectionChanging(this,e);
		}

		/// <summary>
		/// Inserts an item into the collection at the specified index.
		/// </summary>
		/// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
		/// <param name="item">The object to insert.</param>
		protected override void InsertItem(int index, T item)
		{
			var e = new CollectionChangingEventArgs(CollectionChangeAction.Add, index, item);
			OnCollectionChanging(e);

			if (e.Cancel)
				return;

            LogInsert(item);
			base.InsertItem(index, item);
		}

        internal virtual void LogInsert(T item)
        {
        }

        internal virtual void LogRemove(T item)
        {
        }

        /// <summary>
		/// Removes the item at the specified index of the collection.
		/// </summary>
		/// <param name="index">The zero-based index of the element to remove.</param>
		protected override void RemoveItem(int index)
		{
			var e = new CollectionChangingEventArgs(CollectionChangeAction.Remove, index, Items[index]);
			OnCollectionChanging(e);
			
			if (e.Cancel)
				return;

            LogRemove(this[index]);
			base.RemoveItem(index);
		}

        /// <summary>
		/// Removes all items from the collection.
		/// </summary>
		protected override void ClearItems()
		{
			var e = new CollectionChangingEventArgs(CollectionChangeAction.Refresh);
			OnCollectionChanging(e);

			if (e.Cancel)
				return;

            foreach (var item in this)
            {
                LogRemove(item);
            }

            base.ClearItems();
		}

        #region ISuspendedNotifications Members

        /// <summary>
        /// Suspends the notifications.
        /// </summary>
        public virtual void SuspendNotifications()
        {
            _notificationsSuspended = true;
        }

        /// <summary>
        /// Resumes the notifications.
        /// </summary>
        public virtual void ResumeNotifications()
        {
            _notificationsSuspended = false;

            if (IsDirty)
            {
                IsDirty = false;
                OnPropertyChanged(new PropertyChangedEventArgs("Count"));
                OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }

        bool _notificationsSuspended;

        /// <summary>
        /// Gets a value indicating whether change notifications are suspended.
        /// </summary>
        /// <value>
        /// 	<c>True</c> if notifications are suspended, otherwise, <c>false</c>.
        /// </value>
        public bool NotificationsSuspended
        {
            get { return _notificationsSuspended; }
        }

        #endregion
    }
}
